package com.guozinhs.auto.script;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.guozinhs.auto.core.ExecState;
import com.guozinhs.auto.log.Logger;
import com.guozinhs.auto.screen.ScreenUtils;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

/**
 * 执行操作脚本
 *
 * @Author: GuoZi
 * @Date: 2022/1/8 21:25
 */

public class ScriptHandler {

    private final Robot robot;
    private final Logger log;
    private final ExecState execState;

    private Rectangle runningArea;
    private final Random random = new Random(System.currentTimeMillis());

    /**
     * 脚本特殊符号
     */
    private static final String KEY_GLOBAL = "@";
    private static final String KEY_PERCENT = "%";

    /**
     * 鼠标拖动时间间隔(单位:ms)
     */
    private static final int DRAG_INTERVAL = 5;
    /**
     * 鼠标重复定位次数
     */
    private static final int COUNT_MOUSE_MOVE = 10;
    /**
     * 鼠标等待时间
     */
    private static final Long TIME_MS_WAIT_MOUSE = 3000L;
    /**
     * 指令间隔时间
     */
    private static final Long TIME_MS_WAIT_SCRIPT = 50L;
    /**
     * 默认延迟时间(单位:ms)
     */
    private static final Long DEFAULT_DELAY = 500L;
    /**
     * 随机点击区域的范围
     */
    private static final int RANDOM_AREA = 8;

    public ScriptHandler(Robot robot, Logger log, ExecState execState) {
        this.robot = robot;
        this.log = log;
        this.execState = execState;
    }

    /**
     * 根据脚本内容进行操作
     *
     * @param script       脚本内容
     * @param runningArea  程序窗口
     * @param templateArea 匹配区域
     */
    public void handle(String script, Rectangle runningArea, Rectangle templateArea) {
        // 记录操作范围
        this.runningArea = runningArea;
        if (StrUtil.isBlank(script)) {
            return;
        }
        List<String> scriptParams = Arrays.stream(script.split("[ \r\n]"))
                .filter(StrUtil::isNotBlank)
                .collect(Collectors.toList());
        if (ObjectUtil.isEmpty(scriptParams)) {
            return;
        }
        if (templateArea == null) {
            templateArea = runningArea;
        }
        // 鼠标默认起点为匹配区域的中央
        Point point = this.getCenter(templateArea);
        // 获取各个操作指令
        ScriptType scriptType = null;
        List<String> params = new ArrayList<>(8);
        for (String scriptParam : scriptParams) {
            ScriptType currentScriptType = ScriptType.getByName(scriptParam);
            if (currentScriptType == null) {
                if (scriptType == null) {
                    throw new RuntimeException("未知的指令: " + scriptParam);
                }
                // 普通参数
                params.add(scriptParam);
            } else {
                // 操作指令
                if (scriptType != null) {
                    // 执行上一指令
                    this.exec(templateArea, scriptType, params, point);
                }
                scriptType = currentScriptType;
                params.clear();
            }
        }
        if (scriptType != null) {
            // 执行最后一个指令
            this.exec(templateArea, scriptType, params, point);
        }
    }

    /**
     * 执行操作指令 TODO 代码结构待优化
     *
     * @param templateArea 匹配区域
     * @param scriptType   指令类型
     * @param params       参数
     * @param point        默认起点
     */
    private void exec(Rectangle templateArea, ScriptType scriptType, List<String> params, Point point) {
        Rectangle runningArea = this.runningArea;
        // 指令最小间隔时间
        this.delay(TIME_MS_WAIT_SCRIPT);
        // 等待鼠标进入截屏区域
        this.waitMouse();
        switch (scriptType) {
            case move:
                this.checkParams(scriptType, params, 2);
                Point targetPoint = this.parsePoint(runningArea, templateArea, params.get(0), params.get(1));
                point.x = targetPoint.x;
                point.y = targetPoint.y;
                this.moveMouse(targetPoint, true);
                break;
            case click:
                this.checkParams(scriptType, params, 0, 2);
                int n = 1;
                long time = 0L;
                if (ObjectUtil.isNotEmpty(params)) {
                    n = Integer.parseInt(params.get(0));
                    time = Long.parseLong(params.get(1));
                }
                this.click(point, n, time);
                break;
            case wheel:
                this.checkParams(scriptType, params, 1);
                this.wheel(point, Integer.parseInt(params.get(0)));
                break;
            case drag:
                this.checkParams(scriptType, params, 3, 4);
                if (params.size() == 3) {
                    Point dragPoint = this.parsePoint(runningArea, templateArea, params.get(1), params.get(2));
                    this.mouseDrag(point, dragPoint);
                } else {
                    this.mouseDrag(point,
                            Double.parseDouble(params.get(1)),
                            Integer.parseInt(params.get(2)),
                            Integer.parseInt(params.get(3)));
                }
                break;
            case s:
                this.checkParams(scriptType, params, 1);
                String param = params.get(0);
                this.delay(Long.parseLong(param));
                break;
            default:
                break;
        }
    }

    /**
     * 坐标转换
     * <p>
     * 会将转换结果限制在给定区域内
     *
     * @param runningArea  程序运行区域
     * @param templateArea 模板匹配区域
     * @param xParam       横坐标参数
     * @param yParam       纵坐标参数
     * @return 坐标
     */
    private Point parsePoint(Rectangle runningArea, Rectangle templateArea, String xParam, String yParam) {
        double x = this.parsePointDouble(runningArea, templateArea, xParam, true);
        double y = this.parsePointDouble(runningArea, templateArea, yParam, false);
        x = Math.max(x, runningArea.getX());
        x = Math.min(x, runningArea.getX() + runningArea.getWidth() - 1);
        y = Math.max(y, runningArea.getY());
        y = Math.min(y, runningArea.getY() + runningArea.getHeight() - 1);
        return new Point((int) x, (int) y);
    }

    /**
     * 坐标转换
     *
     * @param runningArea  程序运行区域
     * @param templateArea 模板匹配区域
     * @param param        坐标参数
     * @param isX          是否为横坐标
     * @return 坐标数值
     */
    private double parsePointDouble(Rectangle runningArea, Rectangle templateArea, String param, boolean isX) {
        boolean isPercent = false;
        Rectangle execArea = templateArea;
        if (param.startsWith(KEY_GLOBAL)) {
            execArea = runningArea;
            param = param.substring(1);
        }
        if (param.endsWith(KEY_PERCENT)) {
            // 百分比
            isPercent = true;
            param = param.substring(0, param.length() - 1);
        }
        double result;
        try {
            result = Double.parseDouble(param);
        } catch (NumberFormatException e) {
            log.info("脚本参数类型异常, '{}'不是数字", param);
            throw new RuntimeException("脚本参数类型异常");
        }
        double start;
        double length;
        if (isX) {
            start = execArea.getX();
            length = execArea.getWidth();
        } else {
            start = execArea.getY();
            length = execArea.getHeight();
        }
        if (isPercent) {
            start += result * length / 100;
        } else {
            start += result;
        }
        return start / ScreenUtils.getScreenScale();
    }

    /**
     * 参数校验
     *
     * @param scriptType   脚本类型
     * @param params       参数列表
     * @param expectedSize 允许的参数数量
     */
    private void checkParams(ScriptType scriptType, List<String> params, int... expectedSize) {
        boolean success = false;
        for (int size : expectedSize) {
            if (params.size() == size) {
                success = true;
                break;
            }
        }
        if (!success) {
            throw new RuntimeException("参数数量异常: " + scriptType);
        }
    }

    /**
     * 单击左键
     *
     * @param point 坐标点
     */
    private void click(Point point, int n, long time) {
        this.moveMouse(point, true);
        for (int i = 0; i < n; i++) {
            robot.mousePress(KeyEvent.BUTTON1_DOWN_MASK);
            robot.mouseRelease(KeyEvent.BUTTON1_DOWN_MASK);
            if (time > 0 && i != n - 1) {
                time = Math.max(time, this.execState.getGlobalData().getMinClickInterval());
                this.delay(time);
            }
        }
    }

    /**
     * 鼠标移动
     *
     * @param point 坐标点
     */
    private void wheel(Point point, int wheelCount) {
        this.moveMouse(point, true);
        robot.mouseWheel(wheelCount);
    }

    /**
     * 鼠标移动
     * <p>
     * 由于屏幕显示比例问题, 需要多次移动才能较准确地定位
     *
     * @param point 坐标点
     */
    private void moveMouse(Point point) {
        this.moveMouse(point, false);
    }

    /**
     * 鼠标移动
     * <p>
     * 由于屏幕显示比例问题, 需要多次移动才能较准确地定位
     *
     * @param point            坐标点
     * @param enableRandomMove 是否允许区域随机点击
     */
    private void moveMouse(Point point, boolean enableRandomMove) {
        // 随机范围点击
        if (enableRandomMove && execState.isRandomMove()) {
            int radius = this.execState.getRandomMoveRadius();
            DoublePoint steppingPoint = this.getSteppingPoint(random.nextInt(360), random.nextInt(radius));
            int x = (int) (point.x + steppingPoint.getX());
            int y = (int) (point.y + steppingPoint.getY());
            point.setLocation(x, y);
        }
        if (execState.isSmoothMove()) {
            this.moveMouseSmooth(point);
            return;
        }
        for (int i = 0; i < COUNT_MOUSE_MOVE; i++) {
            robot.mouseMove(point.x, point.y);
        }
    }

    /**
     * 鼠标平滑移动
     *
     * @param point 坐标点
     */
    private void moveMouseSmooth(Point point) {
        // 每秒移动500像素
        int speed = this.execState.getSmoothMoveSpeed();
        int distance = speed * DRAG_INTERVAL / 1000;
        // 0.5秒检测一次鼠标是否在操作范围内
        int checkInterval = 500;
        int checkCount = checkInterval / DRAG_INTERVAL;
        int count = 0;
        Point mousePoint = this.getMousePoint();
        DoublePoint targetPoint = this.getSteppingPoint(mousePoint, point, distance);
        double x = mousePoint.getX();
        double y = mousePoint.getY();
        while (true) {
            if (count++ == checkCount) {
                count = 0;
                this.waitMouse();
                // 重新获取鼠标位置
                mousePoint = this.getMousePoint();
                targetPoint = this.getSteppingPoint(mousePoint, point, distance);
                x = mousePoint.getX();
                y = mousePoint.getY();
            }
            double deltaX = Math.abs(x - point.x);
            double deltaY = Math.abs(y - point.y);
            if (deltaX <= distance && deltaY <= distance) {
                break;
            }
            x += targetPoint.getX();
            y += targetPoint.getY();
            robot.mouseMove((int) x, (int) y);
            robot.delay(DRAG_INTERVAL);
        }
    }

    /**
     * 获取鼠标当前坐标
     *
     * @return 坐标信息
     */
    private Point getMousePoint() {
        PointerInfo pointerInfo = MouseInfo.getPointerInfo();
        return pointerInfo.getLocation();
    }

    /**
     * 获取移动指定距离后的偏移坐标
     *
     * @param from     起点
     * @param to       终点
     * @param distance 移动距离
     * @return 偏移坐标
     */
    private DoublePoint getSteppingPoint(Point from, Point to, int distance) {
        double degree = this.getDegree(from, to);
        return getSteppingPoint(degree, distance);
    }

    /**
     * 获取移动指定距离后的偏移坐标
     *
     * @param degree   角度
     * @param distance 移动距离
     * @return 偏移坐标
     */
    private DoublePoint getSteppingPoint(double degree, int distance) {
        double sx = Math.cos(degree * Math.PI / 180) * distance;
        double sy = -(Math.sin(degree * Math.PI / 180) * distance);
        return new DoublePoint(sx, sy);
    }

    /**
     * 鼠标瞬间拖动
     * <p>
     * 拖动后鼠标会回到起点
     *
     * @param from 起点
     * @param to   终点
     */
    private void mouseDrag(Point from, Point to) {
        double degree = this.getDegree(from, to);
        int distance = (int) Math.pow(Math.pow(from.x - to.x, 2) + Math.pow(from.y - to.y, 2), 0.5);
        this.mouseDrag(from, degree, distance, 200);
    }

    /**
     * 鼠标平滑拖动
     * <p>
     * 拖动后鼠标会回到起点
     *
     * @param point    起点
     * @param degree   拖动角度(角度制, 向右为0度 ,逆时针方向为正)
     * @param distance 拖动距离
     * @param ms       拖动时间(单位:ms)
     */
    private void mouseDrag(Point point, double degree, int distance, int ms) {
        int t = DRAG_INTERVAL;
        double nx = point.x;
        double ny = point.y;
        double sx = Math.cos(degree * Math.PI / 180) * distance / ms * t;
        double sy = -(Math.sin(degree * Math.PI / 180) * distance / ms * t);
        this.moveMouse(point);
        robot.mousePress(KeyEvent.BUTTON1_DOWN_MASK);
        int count = ms / t;
        for (int i = 0; i < count; i++) {
            nx += sx;
            ny += sy;
            robot.mouseMove((int) nx, (int) ny);
            robot.delay(t);
        }
        robot.mouseRelease(KeyEvent.BUTTON1_DOWN_MASK);
        this.moveMouse(point);
    }

    /**
     * 等待鼠标进入截屏区域
     */
    public void waitMouse() {
        while (!isMouseNearby() || this.execState.isSuspended()) {
            if (this.execState.isShutdown()) {
                throw new RuntimeException("执行结束");
            }
            try {
                Thread.sleep(TIME_MS_WAIT_MOUSE);
            } catch (InterruptedException ite) {
                throw new RuntimeException("执行结束");
            }
        }
        if (this.execState.isShutdown()) {
            throw new RuntimeException("执行结束");
        }
    }

    /**
     * 判断鼠标是否在截屏范围内
     *
     * @return 判断结果
     */
    public boolean isMouseNearby() {
        Rectangle range = this.runningArea;
        // 获取鼠标位置
        Point location = MouseInfo.getPointerInfo().getLocation();
        int x = location.x;
        int y = location.y;
        // 计算截屏范围
        int minX = range.x;
        int maxX = minX + range.width;
        int minY = range.y;
        int maxY = minY + range.height;
        return x >= minX && x < maxX && y >= minY && y < maxY;
    }

    /**
     * 获取中央坐标
     *
     * @param range 操作范围
     * @return 坐标
     */
    private Point getCenter(Rectangle range) {
        double x = range.getX() + range.getWidth() * 0.5;
        double y = range.getY() + range.getHeight() * 0.5;
        return new Point((int) x, (int) y);
    }

    /**
     * 计算两点间的角度
     *
     * @param from 起点
     * @param to   终点
     * @return 角度(角度制, 向右为0度, 逆时针方向为正)
     */
    private double getDegree(Point from, Point to) {
        int compareX = Double.compare(from.getX(), to.getX());
        int compareY = Double.compare(from.getY(), to.getY());
        boolean sameX = (compareX == 0);
        boolean sameY = (compareY == 0);
        if (sameX && sameY) {
            // 两点相同
            return 0.0;
        }
        if (sameX) {
            // 屏幕的Y轴是向下为正
            return compareY > 0 ? 90 : -90;
        }
        if (sameY) {
            return compareX > 0 ? 180 : 0;
        }
        double degree = -Math.atan((to.getY() - from.getY()) / (to.getX() - from.getX())) * 180 / Math.PI;
        if (compareX > 0) {
            degree += 180;
        }
        return degree;
    }

    /**
     * 等待延迟
     *
     * @param delayMs 等待时间(单位:ms)
     */
    private void delay(Long delayMs) {
        if (delayMs == null || delayMs < 0) {
            delayMs = DEFAULT_DELAY;
        }
        try {
            Thread.sleep(delayMs);
        } catch (InterruptedException ite) {
            ite.printStackTrace();
        }
    }

    @Data
    @AllArgsConstructor
    private static class DoublePoint {
        private double x;
        private double y;
    }

}
